<?php

/*********************************************************************************
 * Copyright (C) 2011-2013 X2Engine Inc. All Rights Reserved.
 *
 * X2Engine Inc.
 * P.O. Box 66752
 * Scotts Valley, California 95067 USA
 *
 * Company website: http://www.x2engine.com
 * Community and support website: http://www.x2community.com
 *
 * X2Engine Inc. grants you a perpetual, non-exclusive, non-transferable license
 * to install and use this Software for your internal business purposes.
 * You shall not modify, distribute, license or sublicense the Software.
 * Title, ownership, and all intellectual property rights in the Software belong
 * exclusively to X2Engine.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
 ********************************************************************************/

/* @edition:pro */

Yii::import('application.components.*');
Yii::import('application.components.util.*');
Yii::import('application.models.*');
Yii::import('application.modules.users.models.*');
Yii::import('application.modules.contacts.models.*');
Yii::import('application.modules.actions.models.*');
Yii::import('application.components.permissions.*');

/**
 * Utilities and methods for the email dropbox and parsing utilities
 *
 * @property string $alias The email address of the alias; value of {@link Admin::$emailDropbox_alias}
 * @property bool $createContact (read-only) Create a contact if none exists already; value of {@link Admin::$emailDropbox_createContact}
 * @property bool $emptyContact (read-only) Create a contact even if no first/last name info was found in the email.
 * @property bool $logging (read-only) Whether to log events.
 * @property bool $zapLineBreaks (read-only) Whether to attempt to remove stray linebreaks; value of  {@link Admin::$emailDropbox_zapLineBreaks}
 * @package X2CRM.components
 * @author Demitri Morgan <demitri@x2engine.com>
 */
class EmailImportBehavior extends CBehavior {

    public static $authorEmail = 'customersupport@x2engine.com';

    /**
     * Email parsing object for the incoming email
     * @var EmlParse
     */
    public $parser;

    /**
     * The mail name used locally
     * @var string
     */
    private $_alias;

    /**
     * Create a contact if none exists
     * @var boolean
     */
    private $_createContact;

    /**
     * Create a contact even if first/last name are blank in the email
     * @var bool
     */
    private $_emptyContact;

    /**
     * Log email capture events in protected/runtime/console.log
     * @var bool
     */
    private $_logging;

    /**
     * User model
     * @var User
     */
    public $user;

    /**
     * Whether to collapse RFC-spec induced linebreaks.
     * @var bool
     */
    private $_zapLineBreaks;

    /**
     * Magic getter for {@link alias}
     * @return string
     */
    public function getAlias(){
        if(!isset($this->_alias)){
            $alias = Yii::app()->params->admin->emailDropbox_alias;
            if(!empty($alias))
                $this->_alias = $alias;
            else{
                // No alias specified by the user.
                //
				// Try to guess the full mail alias including domain name based
                // on the default alias and all the addresses in the To: and CC:
                // fields.
                if(isset($this->parser)){
                    foreach($this->parser->getTo() as $addressee){
                        if(preg_match('/^dropbox@/', $addressee->address)){
                            $this->_alias = $addressee->address;
                            break;
                        }
                    }
                    if(!isset($this->_alias)){
                        foreach($this->parser->getCC() as $addressee){
                            if(preg_match('/^dropbox@/', $addressee)){
                                $this->_alias = $addressee;
                                break;
                            }
                        }
                    }
                } else // There's nothing more that can be done at this point
                    $this->_alias = 'dropbox@localhost';
            }
        }
        return $this->_alias;
    }

    /**
     * Magic setter for {@link alias}
     * @param string $value
     * @return string
     */
    public function setAlias($value){
        return $this->_alias = $value;
    }

    /**
     * Magic getter for {@link createContact}
     * @return bool
     */
    public function getCreateContact(){
        if(!isset($this->_createContact))
            $this->_createContact = (bool) Yii::app()->params->admin->emailDropbox_createContact;
        return $this->_createContact;
    }

    /**
     * Magic getter for {@link log}
     * @return type
     */
    public function getLogging(){
        if(!isset($this->_logging))
            $this->_logging = (bool) Yii::app()->params->admin->emailDropbox_logging;
        return $this->_logging;
    }

    /**
     * Magic getter for {@link zapLineBreaks}
     * @return bool
     */
    public function getZapLineBreaks(){
        if(!isset($this->_zapLineBreaks))
            $this->_zapLineBreaks = (bool) Yii::app()->params->admin->emailDropbox_zapLineBreaks;
        return $this->_zapLineBreaks;
    }

    /**
     * Magic getter for {@link emptyContact}
     * @return bool
     */
    public function getEmptyContact(){
        if(!isset($this->_emptyContact))
            $this->_emptyContact = (bool) Yii::app()->params->admin->emailDropbox_emptyContact;
        return $this->_emptyContact;
    }

    /**
     * Run the dropbox import on a raw email.
     *
     * Convert an email's raw contents, open for reading at file resource object
     * $fh, into contact/action records.
     *
     * @param type $fh
     */
    public function eml2records($fh){
        $data = is_resource($fh) ? stream_get_contents($fh) : $fh;
        // Initialize the parser
        $this->parser = new EmlParse($data);
        $this->parser->zapLineBreaks = $this->zapLineBreaks;
        $addressees = $this->parser->getTo();
        $sender = $this->parser->getFrom();
        $subject = $this->parser->getSubject();
        $senderSubject = array('{sender}' => $sender->address, '{subject}' => $subject);
        $this->log(Yii::t('app','Loaded contents of email from {sender}, subject "{subject}".', $senderSubject));

        // Test that the sender is a valid user:
        $user = Profile::model()->findByAttributes(array('emailAddress' => $sender->address));
        if(!(bool) $user){// User doesn't exist in database; no matching address.
            // Silently log the event and exit, but don't notify the sender.
            $this->log(Yii::t('app','Rejecting email with subject "{subject}" from {sender}; it is from an email address that does not match any user profile and may possibly be spam.', $senderSubject));
            return;
        }
        // Set user for model creation
        $this->user = User::model()->findByAttributes(array('username' => $user->username));

        if(count($addressees)){ // Typically should never be equivalent to false, unless the parser fails.
            $dropboxFwd = false;
            foreach($addressees as $dest){
                // Check to see if the email is addressed directly to the alias.
                // If not, use case 2 will be the case.
                $dropboxFwd = $dropboxFwd || strpos($dest->address, $this->alias) !== false;
                if($dropboxFwd){
                    break;
                }
            }
            if($dropboxFwd){
                // Use case 1: user is forwarding directly to the dropbox capture, so
                // the contact details will need to be parsed from the email body
                // Only operate if the user with that email address exists
                // in the database (to distinguish from spam). Examine the
                // forwarded message.
                $this->log(Yii::t('app','Interpreting email as a forwarded message from a contact or social feed post; email has been sent directly to the alias.'));
                try{
                    $from = $this->parser->getForwardedFrom();
                }catch(Exception $e){
                    if(!preg_match('/^fwd:/i', $this->parser->getSubject())){
                        // Use case 3: Assume the user is *sending* and not
                        // forwarding an email, to put it in as a social post
                        // rather than importing a user's email.
                        $this->log(Yii::t('app','Interpreting email as a social post; it contained no recognized forwarded message patterns and its subject does not indicate that it is a forwarded message.'));
                        return $this->createSocialPost($this->parser->bodyCleanup());
                    }else{
                        $this->log(Yii::t('app','Tried to extract contact info from the attached forwarded message, but no matching patterns are available for extracting it.'));
                        $this->sendErrorEmail('', 'forward');
                    }
                }

                $this->log(Yii::t('app','Successfully parsed contact details from the forwarded message.'));
                $contact = $this->resolveContact($from);

                if((bool) $contact){
                    // Make a new action tied to the contact record
                    $body = "Email sent from contact:\n\n".$this->parser->bodyCleanup();
                    $action = $this->createAction($contact, 'emailFrom');
                    $this->log(Yii::t('app','Created action record for the email.'));
                    if((bool) $action){
                        $this->createEvent($action);
                        $this->log(Yii::t('app','Created activity feed event for the email.'));
                    }
                }
            }else{
                // Use case 2: user is CC-ing to the dropbox capture. Get the contact
                // details from the header fields. Much simpler case.
                // Obtain all destinations:
                $this->log(Yii::t('app','Obtaining contact details from the "To:" field; email has been CC-ed to the alias.'));
                $addressees = $this->parser->getTo();
                foreach($addressees as $recipient){
                    $contact = $this->resolveContact($recipient);
                    if((bool) $contact){
                        $body = $this->parser->bodyCleanup();
                        $action = $this->createAction($contact, 'email');
                        $this->log(Yii::t('app','Created action record for the email.'));
                        if((bool) $action){
                            $this->createEvent($action);
                            $this->log(Yii::t('app','Created activity feed event for the email.'));
                        }
                    }
                }
            }
        }
    }

    /**
     * Record a message in the log.
     *
     * @param string $message Message to record.
     * @param array $params Translation parameters passed to {@link Yii::t()}
     */
    public function log($message){
        if($this->logging)
            Yii::log($message,'trace','application.emailimport');
    }

    /**
     * Creates an action based on the email associated with a given contact.
     */
    public function createAction(Contacts $contact, $type = 'email', $subject = ''){
        if(empty($subject) && !empty($this->parser))
            $subject = $this->parser->getSubject();
        $assignedTo = $this->user->username;
        $action = new Actions('noNotif');
        $action->subject = $subject;
        Yii::app()->setSuModel($this->user);
        $action->associationType = 'contacts';
        $action->associationId = $contact->id;
        $action->associationName = $contact->name;
        $action->assignedTo = $assignedTo;
        $action->completedBy = $assignedTo;
        $action->type = $type;
        $action->visibility = 1;

        if(!empty($this->parser)){
            $action->actionDescription = str_replace("\n", "<br />\n", '<b>'.$this->parser->getSubject().'</b><br />'.$this->parser->bodyCleanup());
        }else{
            // The error case
            $action->actionDescription = '<b>Error</b><br />Email parser object was not initialized, so this email was not properly imported.';
        }
        $action->createDate = time();
        $action->lastUpdated = $action->createDate;
        $action->completeDate = $action->createDate;
        $action->save();
        return $action;
    }

    /**
     * Add a new activity feed item for the email
     * @param type $emailAction
     */
    public function createEvent(Actions $emailAction){
        $typeMap = array(
            'email' => 'email_sent',
            'emailFrom' => 'email_from'
        );
        $event = new Events();
        $event->type = $typeMap[$emailAction->type];
        $event->subtype = 'email';
        $event->associationId = $emailAction->associationId;
        $event->associationType = 'Contacts';
        $event->timestamp = time();
        $event->lastUpdated = $event->timestamp;
        $event->user = $emailAction->assignedTo;
        $event->save();
        return $event;
    }

    /**
     * Create a contact
     * @param type $fullName
     * @param Contacts $contact
     * @return \Contacts
     */
    public function instantiateContact($emlContact){
//		Contacts::check
        $contact = new Contacts();
        Yii::app()->setSuModel($this->user);
        $fullName = EmlRegex::fullName($emlContact->name);
        $contact->email = $emlContact->address;
        $contact->firstName = $fullName[0];
        $contact->lastName = $fullName[1];
        $contact->name = "{$fullName[0]} {$fullName[1]}";
        $contact->visibility = 1;
        $contact->createDate = time();
        $contact->lastUpdated = $contact->createDate;
        $contact->leadtype = 'E-Mail';
        $contact->leadstatus = 'Assigned';
        $contact->assignedTo = $this->user->username;
        $contact->leadSource = 'None';
        $contact->updatedBy = $this->user->username;
        return $contact;
    }

    public function createSocialPost($text){
        $event = new Events();
        $event->type = 'feed';
        $event->subtype = 'Social Post';
        $event->user = $this->user->username;
        $event->text = $text;
        $event->visibility = 1;
        $event->timestamp = time();
        $event->lastUpdated = $event->timestamp;
        $event->save();
        return $event;
    }

    /**
     * Look up a preexisting contact and instantiate/save a new one if one does
     * not exist, based on system settings.
     *
     * @param object $entity An object containing a first/last name and email address
     * @param bool $sendEmail Whether to send an error email if the contact fails validation.
     * @return bool|Contacts
     */
    public function resolveContact($entity, $sendEmail = true){
        $contact = Contacts::model()->findByAttributes(array('email' => $entity->address));
        if(!(bool) $contact){
            // Contact not found. Create new?
            $this->log(Yii::t('app','No preexisting contact matching {email} was found.', array('{email}' => $entity->address)));
            if($this->createContact){
                // Make a new contact
                $contact = $this->instantiateContact($entity);
                $params = array(
                    '{firstName}' => $contact->firstName,
                    '{lastName}' => $contact->lastName,
                    '{email}' => $contact->email,
                    '{errors}' => '',
                );
                if(!$this->emptyContact){ // Check for non-empty first or last name
                    if($contact->firstName == 'UnknownFirstName' || $contact->lastName == 'UnknownLastName'){
                        $this->log(Yii::t('app','Skipping creation of new contact; first or last name was not found in email metadata, and option Admin.emailDropbox_emptyContact is disabled.'));
                        return false;
                    }
                }
                $this->log('Creating new contact: {firstName} {lastName} ({email})', $params);
                if(!$contact->save()){
                    $params['{errors}'] = CJSON::encode($contact->errors);
                    $errorMsg = 'Contact failed validation and/or could not be saved: {firstName} {lastName}, email {email}. Validation errors were as follows: {errors}';
                    $this->log(Yii::t('app','Contact failed validation and/or could not be saved: {firstName} {lastName}, email {email}. Validation errors were as follows: {errors}', $params));
                    if($sendEmail){
                        $this->sendErrorEmail(Yii::t('app', 'Contact failed validation and/or could not be saved: {firstName} {lastName}, email {email}. Validation errors were as follows: {errors}', $params));
                    }
                    return false;
                }
                $this->log(Yii::t('app','Contact saved successfully.'));
            }else{
                // Nope.
                $this->log(Yii::t('app','Skipping creation of new contact; option "Create contacts from emails" is disabled.'));
                return false;
            }
        }
        Yii::app()->setSuModel($this->user);
        return $contact;
    }

    /**
     * In the case of failure, sends a message to the original sender.
     *
     * @param string $message The message.
     * @param string $type The type of error
     * @param bool $send Whether to send the email, or return the PHPMailer object (default: true)
     * @return \PHPMailer
     */
    public function sendErrorEmail($message, $type = null, $send = True){
        $eml = new InlineEmail;
        $emailFrom = Credentials::model()->getDefaultUserAccount(Credentials::$sysUseId['systemNotificationEmail'], 'email');
        if($emailFrom == Credentials::LEGACY_ID){
            $eml->from = array(
                'name' => 'X2CRM Email Capture',
                'address' => Yii::app()->params->admin->emailFromAddr,
            );
        }else{
            $eml->credId = $emailFrom;
        }

        $mail = $eml->mailer;


        $mail->FromName = 'X2Engine Email Capture';
        $fancyDiv = '<div style="font:normal 14px/21px Arial,\'Lucida Sans Unicode\',\'Lucida Grande\',\'Trebuchet MS\',Helvetica,sans-serif;color:#666;">';
        switch($type){
            case 'forward':
                $mail->Subject = 'Unrecognized forwarded email format';
                $msg = $fancyDiv.'<p>The email capture script was not able to recognize the format of the forwarded message, and thus was not able to obtain contact data. Note also that if email is sent directly to the email capture address, it requires that the body contain a forwarded message.</p>';
                $msg .= '<p>Please forward this email, using the email client program that caused the error, to X2Engine Customer Support at '.self::$authorEmail.' so that support for the format can be added. You may redact any information you wish from the original message.</p>';
                $msg .= "<p>The original email's contents were as follows:</p></div>";
                $msg .= '<pre>'.$this->parser->getBody().'</pre>';
                $this->log(Yii::t('app','Failed parsing forwarded email. The contents were as follows:')."\n".$this->parser->getBody());
                // We also should stash the failed email in the database so it
                // can later be used for self-service, i.e. generating new
                // patterns through the app.
                break;
            default:
                $mail->Subject = 'Error while attempting to import data from an email.';
                $msg = $fancyDiv.'An unexpected error occurred while attempting to import an email. The email was:</div>';
                $msg .= "<pre>$message</pre>";
                $msg .= $fancyDiv."The original message was:</div><pre>".$this->parser->getBody()."</pre>";
                $this->log(Yii::t('app','Failed creating a contact. The email was:').$this->parser->getBody());
        }

        $mail->MsgHTML($msg);
        $origSender = $this->parser->getFrom();
        if(is_array($origSender)){
            foreach($origSender as $sender)
                $mail->AddAddress($sender->address);
        } else
            $mail->AddAddress($origSender->address);

        if($send){
            $mail->Send();
            return null;
        }else{
            return $mail;
        }
    }

}

/*
 * The email capture script was not able to recognize the format of the forwarded message, and thus was not able to obtain contact data. Note also that if email is sent directly to the email capture address, it requires that the body contain a forwarded message.

  Please forward this email, using the email client program that caused the error, to X2Engine Customer Support with the subject "Unrecognized forwarded email format" so that support for the format can be added. You may redact any information you wish from the original message.
 *
 */
?>
